<!DOCTYPE html>
<html>
<header>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
  <!-- 调试工具 -->
  <script type="text/javascript" src="./plugins/eruda.js"></script>
  <!-- 二维码解析 -->
  <script src="./plugins/jsQR.js"> </script>
  <!-- 透视变换 -->
  <script src="./plugins/jsfeat-min.js"></script>

  <style>
    @keyframes animate {
      0% {
        color: #ccc;
      }

      100% {
        color: transparent;
      }
    }

    * {
      padding: 0;
      margin: 0;
    }

    ::-webkit-media-controls {
      display: none !important;
    }

    html,
    body {
      width: 100%;
      height: 100%;
      font-size: 16rem;
      overflow: hidden;
      background-color: #fff;
    }

    video {
      font-size: 0;
    }

    canvas {
      position: fixed;
      bottom: 0;
      visibility: hidden;
    }

    .bg {
      background-color: rgba(0, 0, 0, .35);
      color: #fff;
    }

    .flex-row-center {
      display: flex;
      justify-content: center;
      align-items: center;
    }

    .handle-colse {
      position: absolute;
      right: 20rem;
      top: 40rem;
      font-size: 60rem;
      color: #fff;
    }

    .toast {
      padding: 10rem 40rem;
      max-width: 80%;
      min-width: 50%;
      position: fixed;
      bottom: 20%;
      left: 50%;
      z-index: 9999;
      transform: translate(-50%, -50%);
      border: 1px solid rgba(255, 255, 255, .35);
      background: rgba(0, 0, 0, .35);
      color: #fff;
      border-radius: 9999px;
      font-size: 24rem;
      text-align: center;
      display: none;
    }

    .tips {
      font-size: 24rem;
      color: #ccc;
      animation: animate 2s linear infinite;
      display: none;
    }


    .frame {
      width: 200rem;
      height: 200rem;
      position: fixed;
      left: 275rem;
      bottom: 0%;
      z-index: 9999;
      transition: display 0.5s ease-in-out;
      background-color: transparent;
      overflow: hidden;
    }

    .camera {
      position: relative;
      width: 100%;
      height: 100%;
      min-width: 100rem;
      min-height: 100rem;
      overflow: hidden;

      video {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        will-change: transform;
      }
    }

    .cameraMode {
      position: relative;
      width: 100%;
      height: 100%;
    }

    .box-border {
      position: absolute;
      left: 0;
      top: 0;
      right: 0;
      bottom: 0;
      box-sizing: content-box;
    }

    .box-border .box-border-item.box-border-mini {
      width: 25rem;
      height: 25rem;
    }

    .box-border .border-l {
      width: 50rem;
      height: 50rem;
      position: absolute;
      left: 0;
      top: 0;
      border-width: 2rem 0 0 2rem;
      border-style: solid;
      border-color: #4d85ff;
    }

    .box-border .border-t {
      width: 50rem;
      height: 50rem;
      position: absolute;
      top: 0;
      right: 0;
      border-width: 2rem 2rem 0 0;
      border-style: solid;
      border-color: #4d85ff;
    }

    .box-border .border-r {
      width: 50rem;
      height: 50rem;
      position: absolute;
      right: 0;
      bottom: 0;
      border-width: 0 2rem 2rem 0;
      border-style: solid;
      border-color: #4d85ff;
    }

    .box-border .border-b {
      width: 50rem;
      height: 50rem;
      position: absolute;
      left: 0;
      bottom: 0;
      border-width: 0 0 2rem 2rem;
      border-style: solid;
      border-color: #4d85ff;
    }

    .camera-layer {
      display: flex;
      flex-direction: column;
      position: absolute;
      width: 100%;
      height: 100%;
      z-index: 2;
    }

    .camera-layer .camera-view-header {
      width: 100%;
      height: calc((100vh - 400rem) / 2);
    }

    .camera-layer .camera-view-body {
      position: relative;
      width: 100%;
      height: 400rem;
      display: flex;
    }

    .camera-layer .camera-view-body .camera-body-left {
      width: 175rem;
      height: 400rem;
    }

    .camera-layer .camera-view-body .camera-body {
      position: relative;
      width: 400rem;
      height: 400rem;
      display: flex;
      flex-direction: column;
    }

    .camera-layer .camera-view-body .camera-body .assist-name {
      width: 100%;
      height: 50rem;
      position: absolute;
      top: -50rem;
    }

    .camera-layer .camera-view-body .camera-body .assist-name .assist-block-bottom {
      width: 100%;
      height: 50rem;
      font-size: 26rem;
      line-height: 50rem;
      text-align: center;
      color: #fff;
    }

    .camera-layer .camera-view-body .camera-body .assist-code {
      position: relative;
      width: 400rem;
      height: 400rem;
    }

    .camera-layer .camera-view-body .camera-body-right {
      width: 175rem;
      height: 400rem;
    }

    .camera-layer .camera-view-footer {
      height: calc((100vh - 400rem) / 2);
    }
  </style>
</header>

<body>
  <div class="camera">
    <div class="camera-layer">
      <!-- top -->
      <div class="camera-view-header  bg flex-row-center">
        <!-- 提示 -->
        <div id="tips" class="tips"></div>
      </div>
      <!-- center -->
      <div class="camera-view-body">
        <div class="camera-body-left bg"> </div>
        <div class="camera-body">
          <!-- <div class="line">请按照辅助线对准文字</div> -->

          <!-- 标志 -->
          <div class="assist-name">
            <div class="assist-block-bottom"> 请对准要识别的标签 </div>
          </div>
          <!-- 码图 -->
          <div class="assist-code">
            <div class="box-border">
              <div class="border-l"> </div>
              <div class="border-t"> </div>
              <div class="border-r"> </div>
              <div class="border-b"> </div>
            </div>
          </div>
        </div>
        <div class="camera-body-right bg">

        </div>
      </div>
      <!-- bottom -->
      <div class="camera-view-footer flex-row-center bg"> </div>
    </div>

    <!-- 关闭 -->
    <div v-if="close" class="handle-colse iconfont icon-close" @click="handleClose"></div>
    <!-- 相机 -->
    <video id="video" muted autoplay playsinline webkit-playsinline x5-video-player-type="h5"
      x5-video-player-fullscreen="true" style="display: none;"></video>


  </div>
  <!-- 画布 -->
  <canvas id="canvas"></canvas>
  <!-- 矫正画布 -->
  <canvas id="correctCanvas"></canvas>
  <!-- toast -->
  <div id="toast" class="toast"></div>
  <!-- 帧 -->
  <!-- <img id="frame" src="" class="frame"></img> -->
</body>
<!-- 初始化 -->
<script>
  // 初始化 eruda 调试工具
  eruda.init();

  // 设置根字体大小（通常在页面初始化时执行）
  const docEl = document.documentElement;
  let rem = 0;
  const resize = () => {
    const clientWidth = docEl.clientWidth || 375;
    rem = clientWidth / 750;
    docEl.style.fontSize = rem + 'px'; // 750rrem = 100%宽度
  };
  window.addEventListener('resize', resize);
  resize();

</script>
<!-- 工具类 -->
<script>

  /** 对象转get参数 */
  function formatQuery (data) {
    return '?' + Object.keys(data).map(key => {
      return `${key}=${typeof data[key] == 'string' ? encodeURIComponent(data[key]) : data[key]}`
    }).join('&')
  }
  /** 将get参数转对象 */
  function getQuery () {
    const params = {};
    const queryString = window.location.search.substring(1);
    const pairs = queryString.split('&');

    for (let i = 0; i < pairs.length; i++) {
      const pair = pairs[i].split('=');
      params[pair[0]] = decodeURIComponent(pair[1] || '');
    }
    return params;
  }
  /**
    * 显示Toast提示信息
    * text - 要显示的文本内容
    * options - 可选配置对象，可包含time(显示时间)和callback(回调函数)
    */
  function showToast (text, options = {}) {
    const toast = document.getElementById('toast');
    toast.innerText = text;

    toast.style.display = 'block';

    setTimeout(() => {
      options.callback && options.callback();

      toast.style.display = 'none';

    }, options.time || 3000);
  }
  /**
    * 显示Toast提示信息
    * text - 要显示的文本内容
    * options - 可选配置对象，可包含time(显示时间)和callback(回调函数)
    */
  function showTip (text, options = {}) {
    const tips = document.getElementById('tips');
    tips.innerText = text
    tips.style.display = 'block';

    if (options.time) {
      setTimeout(() => {
        options.callback && options.callback();

        tips.style.display = 'none';

      }, options.time || 5000);
    }
  }

</script>
<!-- 请求类 -->
<script>

  //地址参数
  const query = getQuery()
  const apis = {
    190: 'http://192.168.31.190:8000',
    187: 'http://192.168.31.187:8000',
  }
  const api = apis[query.api] || 'https://www.antifakefire.com:8040'

  console.log('get参数', query);

  function UploadCodeImage (blob) {
    return new Promise((resolve, reject) => {
      let data = new FormData()
      data.append('image', blob)
      fetch(api + '/api/watermark/android_qr/up_load', {
        method: 'POST',
        body: data
      }).then(res => resolve(res.json())).catch(err => {
        reject(err)
      })
    })
  }

  function RegisterCodeImage (blob, info) {
    return new Promise((resolve, reject) => {
      let data = new FormData()
      data.append('image', blob)
      data.append('body', info)
      fetch(api + '/api/watermark/register', {
        method: 'POST',
        body: data
      }).then(res => resolve(res.json())).catch(err => {
        reject(err)
      })
    })
  }

  function UploadDeviceInfo (devices) {

    const isSaved = localStorage.getItem('saveDevice')
    console.log('isSaved', isSaved);
    if (isSaved) return

    const fpPromise = import('https://openfpcdn.io/fingerprintjs/v4')
      .then(FingerprintJS => FingerprintJS.load())

    fpPromise
      .then(fp => fp.get())
      .then(result => {
        // This is the visitor identifier:
        const visitorId = result.visitorId

        const devicesInfo = {
          deviceId: visitorId,
          brand: navigator.userAgent.split(')')[0] + ')',
          model: '',
          cameras: devices.map(el => {
            const params = el.getCapabilities && el.getCapabilities() || {}
            return {
              cameraName: el.label,
              resolution: `${params.width && params.width.max || ''}*${params.height && params.height.max || ''}`,
              frameRate: params.frameRate && params.frameRate.max || '',
            }
          })
        }

        console.log('devicesInfo :', devicesInfo)
        let data = new FormData()
        data.append('body', JSON.stringify(devicesInfo));
        fetch(
          api + '/api/device/save', {
          method: 'POST',
          body: data
        }).then(() => {
          localStorage.setItem('saveDevice', true)
        })
      })
  }
</script>
<!-- 类函数 -->
<script>
  class CompatibilityError extends Error {
    constructor(e) {
      super(e), (this.name = 'CompatibilityError'), Object.setPrototypeOf(this, CompatibilityError.prototype)
    }
  }
  class PlatformError extends Error {
    constructor() {
      super('不支持32位版本的微信\n请重新安装64位版本的微信'), (this.name = 'PlatformError')
    }
  }
  class DecoderError extends Error {
    constructor(e) {
      super(e), (this.name = 'DecoderError')
    }
  }

  class cWorker {
    constructor(workUrl) {
      this.worker = new Worker(workUrl);
      this.status = 'init';
      this.workMap = {};
      this.eventMap = {};

      this.worker.onmessage = (event) => {
        console.log('main 收到消息:', event.data)
        const { id, type, data, error } = event.data;


        if (id && this.workMap[id]) {
          if (error) {
            this.workMap[id].reject(error);
          } else {
            this.workMap[id].resolve(data);
          }
          delete this.workMap[id];
        } else if (this.eventMap[type]) {
          if (type === 'ready') this.status = 'ready'
          this.eventMap[type](event.data)
        }
      }
    }
    getId () {
      return Date.now() + Math.random().toString(36).substring(2);
    }
    on (type, callback) {
      this.eventMap[type] = callback;
    }
    postMessage (data) {
      return new Promise((resolve, reject) => {
        const id = this.getId();
        this.workMap[id] = { resolve, reject };
        this.worker.postMessage({ id, ...data }, data);
      });
    }
  }

  // 2K分辨率(2048×1080) 3K分辨率(3036×2024) 4K分辨率(4096×2160)
  const pixel = { TWO_K: 2048, THREE_K: 3036, FOUR_K: 4096 };
  const isMobileDevice = /Android|HarmonyOS|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
  const isAndroidDevice = /Android/i.test(navigator.userAgent)


  class MediaDevice {
    constructor() {
      // 设备
      this.devices = []
    }
    async initDevice () {
      try {
        if (/WeChat\/arm32/i.test(navigator.userAgent)) throw new PlatformError()

        if (
          navigator.mediaDevices === void 0 && (navigator.mediaDevices = {}) &&
          navigator.mediaDevices.enumerateDevices === void 0
        ) throw new CompatibilityError('当前浏览器不支持enumerateDevices API')

        navigator.mediaDevices.getUserMedia === void 0 &&
          (navigator.mediaDevices.getUserMedia = function (e) {
            const sameName = navigator.webkitGetUserMedia || navigator.mozGetUserMedia
            return sameName
              ? new Promise((resolve, reject) => {
                sameName.call(navigator, e, resolve, reject)
              })
              : Promise.reject(new CompatibilityError('当前浏览器不支持getUserMedia API'))
          })

        //初始化摄像权限
        const firstStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
        firstStream.getVideoTracks().forEach(el => el.stop())
        // 枚举设备
        const devices = await navigator.mediaDevices.enumerateDevices();

        // 过滤设备
        this.devices = devices.filter(el => el.kind === 'videoinput')

        console.log('devices: ', devices, this.devices.map(el => el.label));
      } catch (error) {

        'NotAllowedError' === error.name
          ? new DecoderError('摄像机使用权限被拒绝')
          : error instanceof OverconstrainedError
            ? new CompatibilityError(`当前设备无法满足摄像头使用条件（${error.constraint}）`)
            : new CompatibilityError(error.message)

        console.log('initDevice err: ', error);
      }
    }

  }

  class MediaGyro {
    constructor() {
      //是否支持
      this.support = true
      // 稳定持续时间
      this.stableDuration = 1000
      // 采样间隔(ms)
      this.sampleInterval = 100
      // 震动范围
      this.threshold = 3
      //标准事件
      this.callback = null
      //计数器
      this.count = 0

      this.initGyro()
    }
    //初始化
    async initGyro () {

      // 1. 检测支持性
      if ('DeviceOrientationEvent' in window) {
        // 2. iOS 权限请求
        if (typeof DeviceOrientationEvent.requestPermission === 'function') {
          try {
            const permission = await DeviceOrientationEvent.requestPermission();
            if (permission !== 'granted') {
              this.support = false
              return;
            }
          } catch (error) {
            this.support = false
            console.error('error', error, error.message, error.name);
            return;
          }
        } else {
          this.support = false
        }
      }

      if (this.support) {
        this.startGyro()
      }
    }
    // 启动陀螺仪 
    async startGyro (callback) {
      try {
        const history = {
          time: null,
          alpha: 0,
          beta: 0,
          gamma: 0,
        }
        const maxCount = Math.ceil(this.stableDuration / this.sampleInterval)

        console.log('准备监听陀螺仪');
        window.addEventListener('deviceorientation', ({
          alpha,
          beta,
          gamma
        }) => {
          console.log('监听陀螺仪', history);

          if (history.time) {
            if (Date.now() - history.time < this.sampleInterval) return

            if (alpha - history.alpha <= this.threshold && beta - history.beta <= this
              .threshold &&
              gamma - history.gamma <= this.threshold) {
              this.count++
            }

            if (this.count == maxCount) {
              callback && callback()
              this.count = 0
            }

            history.time = Date.now()
            history.alpha = alpha.toFixed(4)
            history.beta = beta.toFixed(4)
            history.gamma = gamma.toFixed(4)

          } else {
            history.time = Date.now()
            history.alpha = alpha.toFixed(4)
            history.beta = beta.toFixed(4)
            history.gamma = gamma.toFixed(4)
          }
        })

      } catch (error) {
        console.error('陀螺仪获取失败:', error);
      }

    }
  }

  class MediaVideo {
    constructor(video) {
      this.video = video
      this.stream = null
      this.canvasSize = null
      this.canvas = document.getElementById('canvas')
      this.correctCanvas = document.getElementById('correctCanvas')
      this.frame = document.getElementById('frame');
    }
    // 启动摄像头
    async startCamera (option = {}) {
      try {
        const confireuse = {
          video: {
            ...option
          }
        }
        const stream = await navigator.mediaDevices.getUserMedia(confireuse);
        this.stream = stream
        return stream
      } catch (error) {
        console.error('摄像头切换失败:', error, error.message, error.constraint, error.name);
        return null
      }
    }
    // 停止摄像头
    stopCamera () {

      this.stream && this.stream.getTracks().forEach(track => track.stop());
      this.video && this.video.srcObject instanceof MediaStream && this.video.srcObject.getTracks().forEach((e) => e.stop())
      this.stream = null;
    }
    // 初始画布
    initCanvas (video) {
      //通过全局rem计算框的size
      const { clientWidth } = document.documentElement
      const { width } = video.getBoundingClientRect()
      const { videoWidth } = video

      const sizeWidth = 400 * (clientWidth / 750)

      // 缩放率
      const scale = videoWidth / width
      // 画布大小
      this.canvasSize = sizeWidth * scale

      canvas.width = this.canvasSize
      canvas.height = this.canvasSize

      console.log(`initCanvas  video实际宽：${Math.floor(width)}, 屏幕宽${Math.floor(clientWidth)}, video像素宽${Math.floor(videoWidth)}, video缩放：${scale}, canvas大小：${Math.floor(this.canvasSize)}`);
    }
    /** 初始化video位置 */
    initVideoPosition () {
      // 获取页面可视区域的宽度和高度
      const { clientWidth, clientHeight } = document.documentElement
      const { videoWidth, videoHeight } = this.video

      // 计算视频显示区域大小：竖屏时取宽度的2/3，横屏时取高度的2/3
      const ratio = clientHeight > clientWidth ? (2 * clientWidth) / 3 : (2 * clientHeight) / 3

      // 计算缩放比例
      const _e = ratio / 480

      // 安卓设备特殊处理
      if (isAndroidDevice) {
        // 设置视频元素的宽度为视频原始宽度乘以缩放比例
        const _t = clientHeight * (1.8 - ((clientWidth * 1.8) / (clientHeight * 1.8)))
        this.video.width = _t
      } else {
        // 其他设备使用CSS transform进行缩放
        this.video.style.transform = `translate(-50%, -50%)scale(${_e})`
      }
    }
    /** 选择 */
    async selectDevice (devices, deviceId, pixel) {
      this.stopCamera()

      const { clientWidth, clientHeight } = document.documentElement
      // 选择设备
      const device = devices.find(el => el.deviceId == deviceId)

      if (!device) return

      const deviceParams = device.getCapabilities && device.getCapabilities()

      const aspectRatio = clientWidth / clientHeight;
      const verticalRatio = clientHeight > clientWidth;

      console.log('设备Id：', deviceId, '设备信息：', device, '设备参数：', deviceParams);

      const cameraParams = {
        facingMode: 'environment',
        deviceId: device.deviceId,
      }
      // 计算宽高比例
      if (!deviceParams) {
        if (verticalRatio) {
          cameraParams.width = Math.round(pixel * aspectRatio);     // 宽度
          cameraParams.height = pixel;                              // 高度
        } else {
          cameraParams.width = pixel;                               // 宽度
          cameraParams.height = Math.round(pixel / aspectRatio);    // 高度
        }
      } else {
        cameraParams.width = deviceParams.width.max || 0;           // 宽度
        cameraParams.height = deviceParams.height.max || 0;         // 高度
      }

      // 安卓设备 切换方向
      if (isAndroidDevice) {
        const cloneWidth = cameraParams.width
        cameraParams.width = cameraParams.height
        cameraParams.height = cloneWidth
      }
      // 打开摄像头
      const stream = await this.startCamera(cameraParams)

      console.log('video : ', video, 'video 视频流: ', stream, 'video 参数: ', cameraParams);

      // 渲染视频流
      if ('srcObject' in video)
        try {
          this.video.srcObject = stream
        } catch (t) {
          if ('TypeError' !== t.name) throw t
          this.video.src = URL.createObjectURL(stream)
        }
      else this.video.src = URL.createObjectURL(stream)
      this.video.play && this.video.play()
      this.video.style.display = 'block'
    }

    calculateSharpness (imageData) {
      // 转换为灰度数据
      const grayData = new Uint8Array(imageData.width * imageData.height);
      for (let i = 0, j = 0; i < imageData.data.length; i += 4, j++) {
        grayData[j] = 0.299 * imageData.data[i] + 0.587 * imageData.data[i + 1] + 0.114 * imageData.data[i + 2];
      }

      const { width, height } = imageData;

      let sum = 0, sum2 = 0, N = width * height;
      for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
          let L = 0;
          if (x > 0 && x < width - 1 && y > 0 && y < height - 1) {
            L = -4 * grayData[y * width + x]
              + grayData[y * width + (x - 1)]
              + grayData[y * width + (x + 1)]
              + grayData[(y - 1) * width + x]
              + grayData[(y + 1) * width + x];
          }
          sum += L;
          sum2 += L * L;
        }
      }
      const mean = sum / N;
      return sum2 / N - mean * mean;
    }
    /** 四通道透视变换 */
    warp_perspective_color (src, dst, transform) {
      const dst_width = dst.width | 0;
      const dst_height = dst.height | 0;
      const src_width = src.width | 0;
      const src_height = src.height | 0;
      const src_data = src.data;
      const dst_data = dst.data;

      // 解构变换矩阵参数
      const [m00, m01, m02, m10, m11, m12, m20, m21, m22] = transform;

      // 预计算边界值
      const src_width_limit = src_width - 1;
      const src_height_limit = src_height - 1;
      const src_stride = src.width * 4;

      for (let i = 0; i < dst_height; ++i) {
        let xs0 = m01 * i + m02;
        let ys0 = m11 * i + m12;
        let ws = m21 * i + m22;

        for (let j = 0; j < dst_width; j++) {
          const sc = 1.0 / ws;
          const xs = xs0 * sc;
          const ys = ys0 * sc;
          const ixs = xs | 0;
          const iys = ys | 0;

          if (xs > 0 && ys > 0 && ixs < src_width_limit && iys < src_height_limit) {
            const a = Math.max(xs - ixs, 0.0);
            const b = Math.max(ys - iys, 0.0);
            const off = (src_stride * iys + (ixs << 2)) | 0;
            const off_bottom = off + src_stride;

            // 使用临时变量减少数组访问
            const r0 = src_data[off];
            const r1 = src_data[off + 4];
            const r2 = src_data[off_bottom];
            const r3 = src_data[off_bottom + 4];

            const g0 = src_data[off + 1];
            const g1 = src_data[off + 5];
            const g2 = src_data[off_bottom + 1];
            const g3 = src_data[off_bottom + 5];

            const b0 = src_data[off + 2];
            const b1 = src_data[off + 6];
            const b2 = src_data[off_bottom + 2];
            const b3 = src_data[off_bottom + 6];

            // 双线性插值计算
            const dst_off = (i * dst.width + j) << 2;
            dst_data[dst_off] = (r0 + a * (r1 - r0)) + b * ((r2 + a * (r3 - r2)) - (r0 + a * (r1 - r0)));
            dst_data[dst_off + 1] = (g0 + a * (g1 - g0)) + b * ((g2 + a * (g3 - g2)) - (g0 + a * (g1 - g0)));
            dst_data[dst_off + 2] = (b0 + a * (b1 - b0)) + b * ((b2 + a * (b3 - b2)) - (b0 + a * (b1 - b0)));
            dst_data[dst_off + 3] = 255;
          }

          xs0 += m00;
          ys0 += m10;
          ws += m20;
        }
      }
    }
    // 拍照
    takeCamera () {
      return new Promise((resolve, reject) => {
        if (this.stream && this.video && this.video.tagName !== 'VIDEO') {
          return reject('拍照失败，画布不存在！')
        }


        const ctx = this.canvas.getContext('2d')
        const correctCtx = this.correctCanvas.getContext('2d');

        const sizeWidth = 400 * rem

        // 屏幕坐标
        const { clientWidth, clientHeight } = document.documentElement
        // video坐标
        const { top, left, width, height } = this.video.getBoundingClientRect()
        const { videoWidth, videoHeight } = this.video

        // 缩放率
        const scale = videoWidth / width

        const start_x = (clientWidth - sizeWidth) / 2
        const start_y = (clientHeight - sizeWidth) / 2

        const video_x = (start_x - left) * scale
        const video_y = (start_y - top) * scale

        ctx.drawImage(this.video, video_x, video_y, this.canvasSize, this.canvasSize, 0, 0, this.canvasSize, this.canvasSize);

        // console.log(`takeCamera: 屏幕宽${Math.floor(clientWidth)} video宽高${Math.floor(videoWidth)}*${Math.floor(videoHeight)} video实际宽高${Math.floor(width)}*${Math.floor(height)}  video位置${Math.floor(top)}*${Math.floor(left)} 取景框XY点${Math.floor(start_x)}*${Math.floor(start_y)} videoXy点${Math.floor(video_x)}*${Math.floor(video_y)} 画布大小${Math.floor(this.canvasSize)} 缩放${scale}`);

        // 图片待矫正
        const imageData = ctx.getImageData(0, 0, this.canvasSize, this.canvasSize) || {};

        const qrCodeInfo = jsQR(imageData.data, imageData.width, imageData.height);

        if (!qrCodeInfo) {
          return reject('二维码不存在！')
        }
        const { topLeftCorner, topRightCorner, bottomRightCorner, bottomLeftCorner } = qrCodeInfo.location;
        // padding
        topLeftCorner.x - 5 > 0 ? topLeftCorner.x -= 5 : (topLeftCorner.x = 0);
        topLeftCorner.y - 5 > 0 ? topLeftCorner.y -= 5 : (topLeftCorner.y = 0);
        topRightCorner.x += 5
        topRightCorner.y - 5 > 0 ? topRightCorner.y -= 5 : (topRightCorner.y = 0);
        bottomRightCorner.x += 5
        bottomRightCorner.y += 5
        bottomLeftCorner.x - 5 > 0 ? bottomLeftCorner.x -= 5 : (bottomLeftCorner.x = 0);
        bottomLeftCorner.y += 5
        // 将检测到的二维码角点作为源点 (从原始图片取点)
        const src_pts = [topLeftCorner, topRightCorner, bottomRightCorner, bottomLeftCorner];

        // 计算目标正方形的尺寸，取检测到的二维码边长的平均值
        const width1 = Math.hypot(topRightCorner.x - topLeftCorner.x, topRightCorner.y - topLeftCorner.y);
        const width2 = Math.hypot(bottomRightCorner.x - bottomLeftCorner.x, bottomRightCorner.y - bottomLeftCorner.y);
        const height1 = Math.hypot(bottomLeftCorner.x - topLeftCorner.x, bottomLeftCorner.y - topLeftCorner.y);
        const height2 = Math.hypot(bottomRightCorner.x - topRightCorner.x, bottomRightCorner.y - topRightCorner.y);

        const qrWidth = Math.max(width1, width2);
        const qrHeight = Math.max(height1, height2);
        console.log('qrSize', qrWidth, qrHeight);


        // 定义目标正方形的角点 (矫正后图片放点的位置)
        const dst_pts = [{ x: 0, y: 0 }, { x: qrWidth, y: 0 }, { x: qrWidth, y: qrHeight }, { x: 0, y: qrHeight }];

        // --- 计算透视变换矩阵 ---
        // 使用 jsfeat 计算从源点到目标点的正向变换矩阵 (src -> dst)
        const forward_warp_mat = new jsfeat.matrix_t(3, 3, jsfeat.F32_t | jsfeat.C1_t);
        const inverse_warp_mat = new jsfeat.matrix_t(3, 3, jsfeat.F32_t | jsfeat.C1_t);
        jsfeat.math.perspective_4point_transform(
          forward_warp_mat,
          src_pts[0].x, src_pts[0].y, dst_pts[0].x, dst_pts[0].y,
          src_pts[1].x, src_pts[1].y, dst_pts[1].x, dst_pts[1].y,
          src_pts[2].x, src_pts[2].y, dst_pts[2].x, dst_pts[2].y,
          src_pts[3].x, src_pts[3].y, dst_pts[3].x, dst_pts[3].y,
        );
        // warp_perspective_color 函数需要从目标到源的逆变换矩阵
        jsfeat.matmath.invert_3x3(forward_warp_mat, inverse_warp_mat);

        // 创建用于存放矫正后图像数据的 ImageData 对象
        const correctedImageData = correctCtx.createImageData(qrWidth, qrHeight);
        // 调用自定义的彩色透视变换函数
        this.warp_perspective_color(imageData, correctedImageData, inverse_warp_mat.data);

        const sharpness = this.calculateSharpness(correctedImageData)

        console.log('清晰度:', sharpness);

        if (sharpness < 150) {
          return reject('图片清晰度不足！')
        }

        this.correctCanvas.width = qrWidth;
        this.correctCanvas.height = qrHeight;
        correctCtx.putImageData(correctedImageData, 0, 0);
        //返回
        if (this.frame) {
          this.frame.src = this.correctCanvas.toDataURL('image/png', 1)
          reject('测试帧')
        } else {
          correctCanvas.toBlob(blob => {
            resolve(blob)
          })
        }
      })
    }
  }
</script>
<!-- 执行类 -->
<script>
  try {
    const video = document.getElementById('video');

    const deviceClass = new MediaDevice('environment')
    console.log('deviceClass', deviceClass);

    const gyroClass = new MediaGyro()
    console.log('gyroClass', gyroClass);

    const videoClass = new MediaVideo(video)
    console.log('videoClass', videoClass);

    //陀螺仪监听自动拍照
    let trigger = true
    let errCount = 0

    // document.addEventListener('WeixinJSBridgeReady', async function () {
    window.onload = async function () {

      try {

        let defaultPixel = isAndroidDevice ? pixel.TWO_K : pixel.THREE_K

        const { clientWidth, clientHeight } = document.documentElement

        console.log('屏幕宽高 :', clientWidth, clientHeight);


        //初始化设备
        await deviceClass.initDevice()

        if (deviceClass.devices.length) {

          video.addEventListener('play', () => {
            console.log('video 播放');
          });
          video.addEventListener('loadedmetadata', () => {
            const { videoWidth, videoHeight } = video;
            console.log(`video 开始：分辨率：${videoWidth}x${videoHeight}`);
            //更新位置
            videoClass.initVideoPosition(video)
            //绘制画布
            videoClass.initCanvas(video)
            startTrigger()
          });
          video.addEventListener('pause', () => {
            console.log('video 暂停');
          });

          // 选中主摄像头
          const rootCamera = deviceClass.devices.find(el => /(微距)/
            .test(el.label)) || deviceClass.devices.find(el => /(主摄像头|后置相机|camera2 0)/
              .test(el.label))

          console.log('主摄：', rootCamera);

          if (rootCamera) {
            await videoClass.selectDevice(deviceClass.devices, rootCamera.deviceId, defaultPixel)
          } else {
            if (isAndroidDevice) {
              await videoClass.selectDevice(deviceClass.devices, deviceClass.devices[deviceClass.devices.length - 1].deviceId, defaultPixel)
            } else {
              const deviceMax = deviceClass.devices.map(el => {
                const device_params = {
                  deviceId: el.deviceId,
                  max: 0
                }
                const _params = el.getCapabilities()
                if (_params) {
                  const width = _params.width ? _params.width.max : 0
                  const height = _params.height ? _params.height.max : 0
                  device_params.max = Math.max(width, height)
                }
                return device_params
              })

              const deviceMaxPixel = Math.max(...deviceMax.map(el => el.max))

              if (deviceMaxPixel >= pixel.FOUR_K) {
                defaultPixel = pixel.FOUR_K
                await videoClass.selectDevice(deviceClass.devices, deviceMax.find(el => el.max == deviceMaxPixel).deviceId, defaultPixel)
              } else if (deviceMaxPixel >= pixel.THREE_K) {
                defaultPixel = pixel.THREE_K
                await videoClass.selectDevice(deviceClass.devices, deviceMax.find(el => el.max == deviceMaxPixel).deviceId, defaultPixel)
              } else {
                await videoClass.selectDevice(deviceClass.devices, deviceMax.find(el => el.max == deviceMaxPixel).deviceId, defaultPixel)
                // throw new Error('设备不支持！')
              }
            }
          }

          function goResultPage (type, data) {
            errCount = 0
            trigger = true
          }

          function goCheck () {
            videoClass.takeCamera().then(blob => {
              UploadCodeImage(blob).then((data) => {
                if (data.judge) {
                  showToast('识别成功！')
                  goResultPage('success', data.goodsBrand)
                } else {
                  switch (data.code) {
                    case 1000: //识别成功
                      showToast('识别成功！')
                      goResultPage('success', data.goodsBrand)
                      break;
                    case 1001: //继续识别
                      errCount = 0
                      showTip('请对准指定标签！')
                      trigger = true
                      break;
                    case 1002: //识别失败
                      errCount++
                      if (errCount == 2) {
                        errCount = 0
                        showToast('识别错误！')
                        goResultPage('error')
                      } else {
                        showTip('请对准指定标签！')
                        trigger = true
                      }
                      break;
                    default:
                      errCount = 0
                      trigger = true
                  }
                }
              })
            }).catch(err => {
              trigger = true
              console.log('takeCamera err', err);
            })
          }

          function goRegister () {
            videoClass.takeCamera().then(blob => {
              RegisterCodeImage(blob, JSON.stringify({
                brandTitle: query.brandTitle,
                productTitle: query.productTitle
              })).then((data) => {
                if (data.judge) {
                  showToast('注册成功！')
                  goResultPage('register', data.goodsBrand)
                } else {
                  switch (data.code) {
                    case 1001: //继续识别
                      errCount = 0
                      showTip('请对准指定标签！')
                      trigger = true
                      break;
                    case 1002: //识别失败
                      errCount++
                      if (errCount == 2) {
                        errCount = 0
                        showToast('识别错误！')
                        goResultPage('error')
                      } else {
                        showTip('请对准指定标签！')
                        trigger = true
                      }
                      break;
                    case 1003: //已注册
                      errCount = 0
                      showToast('该标签已注册！')
                      trigger = true
                      break;
                    case 1004: //注册成功！
                      showToast('注册成功！')
                      goResultPage('register', data.goodsBrand)
                      break;
                    default:
                      errCount = 0
                      trigger = true
                  }
                }
              })
            }).catch(err => {
              trigger = true
              console.log('takeCamera err', err);
            })
          }

          function triggerFn () {

            if (trigger) {
              //停止拍照
              trigger = false

              switch (query.source) {
                case 'check':
                  //检查
                  goCheck()
                  break;
                case 'register':
                  //检查
                  goRegister()
                  break;
                default:
                  goCheck()
              }

            }
          }

          function startTrigger () {
            if (gyroClass.support) {
              //陀螺仪监听自动拍照
              showTip('请保持稳定！', { time: 3000 })
              gyroClass.startGyro(() => {
                triggerFn()
              })
            } else {
              setInterval(() => {
                triggerFn()
              }, 3000)
            }
          }
        } else {
          alert('该设备不支持验码！')
        }

        document.addEventListener('visibilitychange', function () {
          console.log('页面可见状态：', document.visibilityState);
          if (document.visibilityState === 'visible') {
            // 页面可见时的操作
            trigger = true
            // 播放
            video.play && video.play()
            video.style.display = 'block'
          } else {
            // 页面隐藏时的操作
            trigger = false
            video.style.display = 'none'
          }
          //更新状态
          showTip('请对准指定标签！')
          errCount = 0
        });

      } catch (error) {
        console.log('camera render error:', error);
      }
      // });
    }
  } catch (err) {
    console.log('err', err)
  }
</script>

</html>